/*
 * Copyright 2007 Brian Watkins
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package bw.util;

import java.util.*;
import java.io.*;
import org.json.*;
import bw.net.*;

public class Geocoder
{
    private static final String GOOGLE_BASE_URI = "http://maps.google.com/maps/geo";
    private static final String GOOGLE_QUERY = "q";
    private static final String GOOGLE_OUTPUT = "output";
    private static final String GOOGLE_XML_OUTPUT = "xml";
    private static final String GOOGLE_JSON_OUTPUT = "json";
    private static final String GOOGLE_KEY = "key";

    private static final String GOOGLE_PLACEMARK = "Placemark";

    private static final String GOOGLE_STATUS_CODE = "Status.code";
    private static final String GOOGLE_COUNTRY = "AddressDetails.Country.CountryNameCode";
    private static final String GOOGLE_STREET = "AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.Thoroughfare.ThoroughfareName";
    private static final String GOOGLE_CITY = "AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.LocalityName";
    private static final String GOOGLE_STATE = "AddressDetails.Country.AdministrativeArea.AdministrativeAreaName";
    private static final String GOOGLE_ZIP = "AddressDetails.Country.AdministrativeArea.SubAdministrativeArea.Locality.PostalCode.PostalCodeNumber";
    private static final String GOOGLE_COORDINATES = "Point.coordinates";

    private static final int GOOGLE_CODE_SUCCESS = 200;
    private static final int GOOGLE_CODE_UNKNOWN_LOC = 602;

    private String _googleKey = null;
    private Hashtable _cache = new Hashtable();

    public Geocoder ( String googleKey )
    {
        _googleKey = googleKey;
    }
    
    public Location geocode ( String s )
    {
        //we'll use google maps for geocoding purposes
        //but we also have a cache for commonly used locations
        Location loc = (Location) _cache.get( s );
        
        if ( loc == null )
        {
            //look it up using google.
            loc = requestGeocode( s );

            if ( loc == null )
            {
                //if we still couldn't find anything then treat it as a placename
                loc = new Location();
                loc.place = s;
            }

            _cache.put( s, loc );
        }

        return loc;
    }

    private Location requestGeocode ( String s )
    {
        Location location = null;
        
        //make the request to google
        String geocodeURL = constructURL( s );

        Log.getInstance().write( "url: " + geocodeURL );

        try
        {
            NetResource res = new NetResource( geocodeURL );

            StringBuffer buf = new StringBuffer();
            BufferedReader bin = new BufferedReader( new InputStreamReader( res.getInputStream() ) );
            String line = null;
            while ( ( line = bin.readLine() ) != null )
            {
                buf.append( line );
            }
            bin.close();

            location = parseGeocodeRequest( buf.toString() );
        }
        catch ( Exception ex )
        {
            Log.getInstance().write( "Problem with geocode request: " + geocodeURL, ex );
        }

        return location;
    }

    private Location parseGeocodeRequest ( String jsondoc )
    {
        Location location = null;

        Log.getInstance().write( "doc: \n" + jsondoc );

        try
        {
            JSONObject doc = new JSONObject( jsondoc );
            int code = Integer.parseInt( getJSONString( doc, GOOGLE_STATUS_CODE ) );
            if ( code == GOOGLE_CODE_SUCCESS )
            {
                JSONArray placemarks = doc.getJSONArray( GOOGLE_PLACEMARK );
                
                //just use the first placemark -- there could be multiple though ...
                JSONObject placedoc = placemarks.getJSONObject( 0 );
                
                location = new Location();
                location.country = getJSONString( placedoc, GOOGLE_COUNTRY );
                location.state = getJSONString( placedoc, GOOGLE_STATE );
                location.city = getJSONString( placedoc, GOOGLE_CITY );
                location.zip = getJSONString( placedoc, GOOGLE_ZIP );
                location.street = getJSONString( placedoc, GOOGLE_STREET );
                location.coordinates = getJSONString( placedoc, GOOGLE_COORDINATES );
            }
        }
        catch ( Exception ex ) 
        {
            Log.getInstance().write( "Problem parsing doc: " + jsondoc, ex );
        }

        return location;
    }

    private String constructURL ( String query )
    {
        NetURI uri = new NetURI( GOOGLE_BASE_URI );
        uri.addParam( GOOGLE_QUERY, query );
        uri.addParam( GOOGLE_OUTPUT, GOOGLE_JSON_OUTPUT );
        uri.addParam( "oe", "utf8" );
        uri.addParam( "sensor", "false" );
        uri.addParam( GOOGLE_KEY, _googleKey );

        return uri.toString();
    }

    private String getJSONString( JSONObject doc, String key ) 
        throws JSONException
    {
        String str = "";

        JSONObject jo = doc;

        StringTokenizer st = new StringTokenizer( key, "." );
        while ( st.hasMoreTokens() )
        {
            String k = st.nextToken();
            if ( st.hasMoreTokens() )
            {
                //note: you might think it would be good to just break
                //out of the loop at this point. To not do so however solves
                //a problem with google's json format. Sometimes if you don't specify
                //a street address there is no SubAdministrativeArea (ie no zip code area)
                //but still there is a locality object -- just apended to AdministrativeArea
                //instead. So we just skip over any objects we can't find and keep looking.
                //unpretty but efficient
                if ( jo.has( k ) )
                {
                    jo = jo.getJSONObject( k );
                }
            }
            else 
            {
                if ( jo.has( k ) )
                {
                    str = jo.getString( k );
                }
            }
        }

        return str;
    }

    public static void main ( String [] args )
    {
        if ( args.length != 1 )
        {
            System.out.println( "Geocoder <query>" );
            System.exit( 0 );
        }
        
        Geocoder geocoder = new Geocoder( "ABQIAAAAO6Ew3z6z-OZz5zBYDGQVlhT2yXp_ZAY8_ufC3CFXhHIE1NvwkxRjkPMAJjw-ae1sM7eisPFOGsVXRw" );
        
        Location loc = geocoder.geocode( args[0] );

        System.out.println( "------------------------" );
        System.out.println( loc.toString() );
        System.out.println( "------------------------" );
    }
}